LangChainを使ってSalesforceの顧客データを曖昧に検索する(Node.js使用)
はじめに
ChatGPT APIのFunction callingとNode.jsで処理をオートで振り分けて幸せになるで紹介したSlack Botツールにて、問い合わせ対象の顧客名を部分一致で指定する必要がありました。
これは、顧客の検索時に顧客名の指定を内部的にSalesforceのSOQLで行っていたためです。
SELECT Id FROM Account WHERE Name LIKE '%対象顧客名%'
そのため、部分的にではあってもSalesforceの顧客名と一致する名前を正確に指定する必要がありました。また、顧客の業界などで検索することもできません。
そこで、もっと顧客を曖昧に指定したいと考え、LLMを用いたアプリケーション開発を効率的に行うためのライブラリLangChainを使って、Salesforceの顧客データをLLMとして取り込み、曖昧検索できるようにしてみました。
Slack BotをNode.jsで開発していた経緯から、頻繁に用いられるPythonではなくNode.jsからLangChainを使っています。Node.jsによるLangChainの使い方のサンプルとしてもご参考になればと思います。
コード
import jsforce from 'jsforce' import { ChatOpenAI } from "langchain/chat_models/openai"; import { SystemMessage, HumanMessage } from "langchain/schema"; import { OpenAIEmbeddings } from "@langchain/openai"; import { MemoryVectorStore } from "langchain/vectorstores/memory"; import { createStuffDocumentsChain } from "langchain/chains/combine_documents"; import { ChatPromptTemplate, HumanMessagePromptTemplate, SystemMessagePromptTemplate } from "@langchain/core/prompts"; import { createRetrievalChain } from "langchain/chains/retrieval"; import { Document } from "@langchain/core/documents"; import { RecursiveCharacterTextSplitter } from "langchain/text_splitter"; /* Salesforceのコネクションを得る */ const conn = new jsforce.Connection({ oauth2 : { clientId: process.env.SALESFORCE_CLIENT_ID, clientSecret: process.env.SALESFORCE_CLIENT_SECRET, } }); await conn.login(process.env.SALESFORCE_USERNAME, process.env.SALESFORCE_PASSWORD, async function(e, userInfo) { if ( e ) { return console.error(e); } }); /* Salesforceから顧客情報を取得する */ const accountCandidates = await getAccountCandidates(); /* 取得した顧客情報を使ってLLMを構築する */ const docs = accountCandidates.map((candidate) => { return new Document({ pageContent: `顧客名 = ${candidate.name}, 業種 = ${candidate.industry}, Webサイト = ${candidate.website}` }); }); const splitter = new RecursiveCharacterTextSplitter({ chunkSize: 1000 }); const splitDocs = await splitter.splitDocuments(docs); const vectorstore = await MemoryVectorStore.fromDocuments( splitDocs, new OpenAIEmbeddings({ openAIApiKey: process.env.OPENAI_API_KEY, model: process.env.OPENAI_GPT_MODEL, max_tokens: process.env.OPENAI_GPT_MAX_TOKENS, temperature: 0 }) ); /* プロンプト定義 */ const prompt = ChatPromptTemplate.fromPromptMessages([ SystemMessagePromptTemplate.fromTemplate("あなたは顧客情報の管理者です。"), HumanMessagePromptTemplate.fromTemplate(` <context> {context} </context> Input: {input} `) ]); /* LLMとプロンプトをセット */ const documentChain = await createStuffDocumentsChain({ llm: new ChatOpenAI({ openAIApiKey: process.env.OPENAI_API_KEY, model: process.env.OPENAI_GPT_MODEL, max_tokens: process.env.OPENAI_GPT_MAX_TOKENS, temperature: 0 }), prompt, }); const retrievalChain = await createRetrievalChain({ combineDocsChain: documentChain, retriever: vectorstore.asRetriever(), }); /* プロンプトにクエリを投げて、顧客のリストを得る */ const result = await retrievalChain.invoke({ input: `${searchKey}、または${searchKey}から類推できる顧客名があれば返してください。\n該当する顧客名が見つからない時は空のリスト([])を返してください。\ne.g. ["顧客名A","顧客名B",...,"顧客名Z"]\n` }); accounts = JSON.parse(JSON.parse(JSON.stringify(result)).answer); /* 顧客名候補取得関数 */ const getAccountCandidates = async () => { return new Promise(async (resolve, reject) => { const records = []; await conn.query(` SELECT Name, AWSIndustry__c, WebsiteCopy__c FROM Account ORDER BY Name ASC `) .on("record", (record) => { records.push({ name: record.Name, industry: record.AWSIndustry__c || '', website: record.WebsiteCopy__c || '' }); }) .on("end", () => { resolve(records); }) .on("error", (err) => { console.error(err); reject(err); }) .run({ autoFetch : true, maxFetch : 30000 });; }); };
概説
Salesforceへの接続、操作にはjsforceを使っています。
コードの大まかな流れとしては次の通りです。
- Salesforceから顧客データを取得する(
getAccountCandidates
関数) 顧客名 = <顧客名>, 業種 = <業種>, Webサイト = <WebサイトURL>
という情報からなるDocumentオブジェクトを取得した顧客データから作成し、LLMを構築- プロンプトを定義して、作成したLLMに問い合わせる
以上で、顧客名の完全一致でなくとも、対象の顧客候補を取得したり、業界やドメインで検索したりできるようになりました。
とはいえ、顧客データが数万のオーダーなので、学習量が足りず、現時点では精度はあまり良くないです。
今後も地道に改善していきたいと思います。